#if defined(__APPLE__)

#include <sys/param.h>
#include <stdint.h>

#include <CoreFoundation/CoreFoundation.h>
#include <IOKit/IOKitLib.h>
#include <IOKit/serial/IOSerialKeys.h>
#include <IOKit/IOBSD.h>

#include <iostream>
#include <string>
#include <vector>

#include "serial/serial.h"

using serial::PortInfo;
using std::string;
using std::vector;

#define HARDWARE_ID_STRING_LENGTH 128

string cfstring_to_string(CFStringRef cfstring);
string get_device_path(io_object_t& serial_port);
string get_class_name(io_object_t& obj);
io_registry_entry_t get_parent_iousb_device(io_object_t& serial_port);
string get_string_property(io_object_t& device, const char* property);
uint16_t get_int_property(io_object_t& device, const char* property);
string rtrim(const string& str);

string
cfstring_to_string(CFStringRef cfstring)
{
  char cstring[MAXPATHLEN];
  string result;

  if (cfstring)
  {
    Boolean success = CFStringGetCString(cfstring,
      cstring,
      sizeof(cstring),
      kCFStringEncodingASCII);

    if (success)
      result = cstring;
  }

  return result;
}

string
get_device_path(io_object_t& serial_port)
{
  CFTypeRef callout_path;
  string device_path;

  callout_path = IORegistryEntryCreateCFProperty(serial_port,
    CFSTR(kIOCalloutDeviceKey),
    kCFAllocatorDefault,
    0);

  if (callout_path)
  {
    if (CFGetTypeID(callout_path) == CFStringGetTypeID())
      device_path = cfstring_to_string(static_cast<CFStringRef>(callout_path));

    CFRelease(callout_path);
  }

  return device_path;
}

string
get_class_name(io_object_t& obj)
{
  string result;
  io_name_t class_name;
  kern_return_t kern_result;

  kern_result = IOObjectGetClass(obj, class_name);

  if (kern_result == KERN_SUCCESS)
    result = class_name;

  return result;
}

io_registry_entry_t
get_parent_iousb_device(io_object_t& serial_port)
{
  io_object_t device = serial_port;
  io_registry_entry_t parent = 0;
  io_registry_entry_t result = 0;
  kern_return_t kern_result = KERN_FAILURE;
  string name = get_class_name(device);

  // Walk the IO Registry tree looking for this devices parent IOUSBDevice.
  while (name != "IOUSBDevice")
  {
    kern_result = IORegistryEntryGetParentEntry(device,
      kIOServicePlane,
      &parent);

    if (kern_result != KERN_SUCCESS)
    {
      result = 0;
      break;
    }

    device = parent;

    name = get_class_name(device);
  }

  if (kern_result == KERN_SUCCESS)
    result = device;

  return result;
}

string
get_string_property(io_object_t& device, const char* property)
{
  string property_name;

  if (device)
  {
    CFStringRef property_as_cfstring = CFStringCreateWithCString(
      kCFAllocatorDefault,
      property,
      kCFStringEncodingASCII);

    CFTypeRef name_as_cfstring = IORegistryEntryCreateCFProperty(
      device,
      property_as_cfstring,
      kCFAllocatorDefault,
      0);

    if (name_as_cfstring)
    {
      if (CFGetTypeID(name_as_cfstring) == CFStringGetTypeID())
        property_name = cfstring_to_string(static_cast<CFStringRef>(name_as_cfstring));

      CFRelease(name_as_cfstring);
    }

    if (property_as_cfstring)
      CFRelease(property_as_cfstring);
  }

  return property_name;
}

uint16_t
get_int_property(io_object_t& device, const char* property)
{
  uint16_t result = 0;

  if (device)
  {
    CFStringRef property_as_cfstring = CFStringCreateWithCString(
      kCFAllocatorDefault,
      property,
      kCFStringEncodingASCII);

    CFTypeRef number = IORegistryEntryCreateCFProperty(device,
      property_as_cfstring,
      kCFAllocatorDefault,
      0);

    if (property_as_cfstring)
      CFRelease(property_as_cfstring);

    if (number)
    {
      if (CFGetTypeID(number) == CFNumberGetTypeID())
      {
        bool success = CFNumberGetValue(static_cast<CFNumberRef>(number),
          kCFNumberSInt16Type,
          &result);

        if (!success)
          result = 0;
      }

      CFRelease(number);
    }

  }

  return result;
}

string rtrim(const string& str)
{
  string result = str;

  string whitespace = " \t\f\v\n\r";

  std::size_t found = result.find_last_not_of(whitespace);

  if (found != std::string::npos)
    result.erase(found + 1);
  else
    result.clear();

  return result;
}

vector<PortInfo>
serial::list_ports(void)
{
  vector<PortInfo> devices_found;
  CFMutableDictionaryRef classes_to_match;
  io_iterator_t serial_port_iterator;
  io_object_t serial_port;
  mach_port_t master_port;
  kern_return_t kern_result;

  kern_result = IOMasterPort(MACH_PORT_NULL, &master_port);

  if (kern_result != KERN_SUCCESS)
    return devices_found;

  classes_to_match = IOServiceMatching(kIOSerialBSDServiceValue);

  if (classes_to_match == NULL)
    return devices_found;

  CFDictionarySetValue(classes_to_match,
    CFSTR(kIOSerialBSDTypeKey),
    CFSTR(kIOSerialBSDAllTypes));

  kern_result = IOServiceGetMatchingServices(master_port, classes_to_match, &serial_port_iterator);

  if (KERN_SUCCESS != kern_result)
    return devices_found;

  while ((serial_port = IOIteratorNext(serial_port_iterator)))
  {
    string device_path = get_device_path(serial_port);
    io_registry_entry_t parent = get_parent_iousb_device(serial_port);
    IOObjectRelease(serial_port);

    if (device_path.empty())
      continue;

    PortInfo port_info;
    port_info.port = device_path;
    port_info.description = "n/a";
    port_info.hardware_id = "n/a";

    string device_name = rtrim(get_string_property(parent, "USB Product Name"));
    string vendor_name = rtrim(get_string_property(parent, "USB Vendor Name"));
    string description = rtrim(vendor_name + " " + device_name);
    if (!description.empty())
      port_info.description = description;

    string serial_number = rtrim(get_string_property(parent, "USB Serial Number"));
    uint16_t vendor_id = get_int_property(parent, "idVendor");
    uint16_t product_id = get_int_property(parent, "idProduct");

    if (vendor_id && product_id)
    {
      char cstring[HARDWARE_ID_STRING_LENGTH];

      if (serial_number.empty())
        serial_number = "None";

      int ret = snprintf(cstring, HARDWARE_ID_STRING_LENGTH, "USB VID:PID=%04x:%04x SNR=%s",
        vendor_id,
        product_id,
        serial_number.c_str());

      if ((ret >= 0) && (ret < HARDWARE_ID_STRING_LENGTH))
        port_info.hardware_id = cstring;
    }

    devices_found.push_back(port_info);
  }

  IOObjectRelease(serial_port_iterator);
  return devices_found;
}

#endif // defined(__APPLE__)
